iT邦幫忙

2024 iThome 鐵人賽

DAY 24
0
JavaScript

Signal API in Angular系列 第 24

Day 24 - 使用 outputToObservable 函數將 OutputEmitterRef 轉換為Observable

  • 分享至 

  • xImage
  •  

output 函數傳回一個 OutputEmitterRef,其他使用者可以訂閱該值來操作該值。 除了 subscribe之外,OutputEmitterRef 還可以使用 outputToObservable 函數轉換為 Observable。此函數與 toSignaltoObservable 一起位於 rxjs-interop 套件中。

一種用例是組件向父組件發出一個值,父組件使用該值發出 HTTP 請求以從伺服器檢索資料。

我們重構 Star War 應用程式來看看它是如何完成的。 當使用者選擇一個角色並點擊按鈕時,該組件會將 id 傳送到 App 組件。 App 組件呼叫後端來檢索 Star War 角色並將資料指派給 StarWarCharacterComponent 組件的 input

引導應用程式

import { provideExperimentalZonelessChangeDetection } from '@angular/core';
import { provideHttpClient } from '@angular/common/http';

export const appConfig = {
 providers: [
   provideHttpClient(),
   provideExperimentalZonelessChangeDetection()
 ]
}
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app.config';

bootstrapApplication(App, appConfig);

提供 Http clientzoneless 功能,並引導應用程式設定。

新增獲取 StarWar 資料的函數

import { catchError, map, of } from 'rxjs';
import { inject, runInInjectionContext, Injector } from '@angular/core';
import { HttpClient } from '@angular/common/http';

export type Person = {
 name: string;
 height: string;
 mass: string;
 hair_color: string;
 skin_color: string;
 eye_color: string;
 gender: string;
 films: string[];
}

const URL = 'https://swapi.dev/api/people';

export function getPerson(id: number, injector: Injector) {
 return runInInjectionContext(injector, () => {
   const http = inject(HttpClient);
   return http.get<Person>(`${URL}/${id}`).pipe(
     map((p) => ({ ...p, id })),
     catchError((err) => {
       console.error(err);
       return of(undefined);
     }));
 });
}

getPerson 函數透過 id 檢索星際大戰角色。

創建一個 StarWarCharacterComponent

import { Person } from "./star-war.api";

export type ListItem = {
 id: number;
 name: string;
}

export type PersonWithId = Person & { id: number };
import { ChangeDetectionStrategy, Component, input, output } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { PersonWithId } from './star-war.type';

@Component({
 selector: 'app-star-war-character',
 standalone: true,
 imports: [FormsModule],
 template: `
   <div class="border">
     @if(person(); as person) {
       <p>Id: {{ person.id }} </p>
       <p>Name: {{ person.name }}</p>
       <p>Height: {{ person.height }}</p>
       <p>Mass: {{ person.mass }}</p>
       <p>Hair Color: {{ person.hair_color }}</p>
       <p>Skin Color: {{ person.skin_color }}</p>
       <p>Eye Color: {{ person.eye_color }}</p>
       <p>Gender: {{ person.gender }}</p>
       <button (click)=”clone.emit(person.id)>Clone me</button>
     } @else {
       <p>No info</p>
     }
   </div>
 `,
})
export class AppStarWarCharacterComponent {
 person = input<undefined | PersonWithId>(undefined);
 clone = output<number>();
}

AppStarWarCharacterComponent 有一個 person input,顯示人物的詳細資料。

新增下拉清單組件以選定 id

import { ChangeDetectionStrategy, Component, input, output, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { ListItem } from './star-war.type';

@Component({
 selector: 'app-star-war-list',
 standalone: true,
 imports: [FormsModule],
 template: `
   <select [(ngModel)]="jediId">
     <option value="0">---select---</option>
     @for (p of persons(); track p.id) {
       <option [value]="p.id">{{ p.name }}</option>
     }
   </select>
   <button (click)="id.emit(jediId())" [disabled]="jediId() === 0">Add a character</button>
 `,
 changeDetection: ChangeDetectionStrategy.OnPush,
})
export class StarWarListComponent {
 persons = input<ListItem[]>([]);
 jediId = signal(0);
 id = output<number>();
}

當使用者從下拉清單中選擇一個值並點擊按鈕時,id output 會將該值傳送到父組件。

將獨立元件匯入到 App 組件中

import { ChangeDetectionStrategy, Component, ComponentRef, effect, inject, Injector, OnDestroy, signal, viewChild, ViewContainerRef } from '@angular/core';
import { AppStarWarListComponent } from './star-war/star-war-list.component';
import { ListItem, PersonWithId } from './star-war/star-war.type';
import { outputToObservable } from '@angular/core/rxjs-interop';
import { getPerson } from './star-war/star-war.api';
import { switchMap } from 'rxjs';

@Component({
 selector: 'app-root',
 standalone: true,
 imports: [AppStarWarListComponent],
 template: `
   <div class="container">
     <ng-container #vcr />
   </div>
   <app-star-war-list [persons]="jedis()" />
 `,
})
export class App implements OnDestroy {
 jedis = signal([
   { id: 1, name: 'Luke' },
   { id: 10, name: 'Obi Wan Kenobe' },
   { id: 20, name: 'Yoda' },
 ] as ListItem[]);

 list = viewChild.required(StarWarListComponent);
 injector = inject(Injector);
 vcr = viewChild.required('vcr', { read: ViewContainerRef });
 componentRefs = [] as ComponentRef<any>[];

 constructor() {
   effect((OnCleanUp) => {
     const sub = outputToObservable(this.list().id)
       .pipe(switchMap((id) => getPerson(id, this.injector)))
       .subscribe((person) => this.addAJedi(person));

     OnCleanUp(() => sub.unsubscribe());
   });
 }

 async addAJedi(person: PersonWithId | undefined) {
   const { AppStarWarCharacterComponent } = await import ('./star-war/star-war-character.component');
   AppStarWarCharacterComponent
   const componentRef = this.vcr().createComponent(AppStarWarCharacterComponent);
   componentRef.setInput('person', person);
   this.componentRefs.push(componentRef);
 }

 ngOnDestroy(): void {
   if (this.componentRefs) {
     for (const ref of this.componentRefs) {
       ref.destroy();
     }
   }
 }
}
list = viewChild.required(AppStarWarListComponent);

App 組件使用 viewChild 函數來查詢 AppStarWarListComponent 組件。

constructor() {
   effect((OnCleanUp) => {
     const sub = outputToObservable(this.list().id)
       .pipe(
         switchMap((id) => getPerson(id, this.injector))
       )
       .subscribe((person) => this.addAJedi(person));

     OnCleanUp(() => sub.unsubscribe());
   });
}

this.list().id input signal 是 effect 的依賴項 (dependency),outputToObservable 函數將其轉換為 Observable。 當使用者從下拉清單中選擇一個值時,效果將執行邏輯。 Observableid 傳送給 switchMap 運算子來呼叫 Star War API 並訂閱 Observable 以獲得結果。 回呼函數 (callback) 會動態匯入 AppStarWarCharacterComponent 組件,將結果指派給 input signal,並將組件附加到 ViewContainerRefsubscribe 方法建立一個訂閱 (subscription);因此,OnCleanUp 回呼 (callback) 會在 effect 銷毀之前取消訂閱 (subscription)。

ngOnDestroy(): void {
   if (this.componentRefs) {
     for (const ref of this.componentRefs) {
       ref.destroy();
     }
   }
}

當應用程式銷毀 App 組件時,ngOnDestroy 會釋放 componentRefs 的記憶體以避免 memory leaks。

使用outputToObservable函數複製動態組件

<button (click)="clone.emit(person.id)">Clone me</button>
export class AppStarWarCharacterComponent {
 person = input<undefined | PersonWithId>(undefined);
 clone = output<number>();
}

AppStarWarCharacterComponent 中有一個 Clone me 按鈕可以複製自身。點選按鈕時,clone output會將 id 傳送到 App 組件。

destroyRef = inject(DestroyRef);

async addAJedi(person: PersonWithId | undefined) {
   const { AppStarWarCharacterComponent } = await import ('./star-war/star-war-character.component');
   const componentRef = this.vcr().createComponent(AppStarWarCharacterComponent);
   componentRef.setInput('person', person);
   outputToObservable(componentRef.instance.clone)
     .pipe(
       switchMap((id) => getPerson(id, this.injector)),
       takeUntilDestroyed(this.destroyRef),
     )
     .subscribe((aClone) => this.addAJedi(aClone));
   this.componentRefs.push(componentRef);
 }

outputToObservable 函數將 componentRef.instance.clone OutputRef 轉換為 Observable 以發出新的 HTTP 請求,並將結果附加到 ViewContainerReftakeUntilDestroyed 運算子使用DestroyRefApp 被銷毀時完成 Observable

結論:

  • outputToObservableoutput 轉換為 Observable,以便該值可以傳送給 RxJS 運算符
  • outputToObservable 駐留在 rxjs-interop 套件中,就像 toSignaltoObservable 一樣
  • 當我們使用 outputToObservable 創建 Observable 並訂閱 (subscribe) 它時,我們需要取消訂閱 (subscription)。

鐵人賽的第 24 天到此結束。

參考:


上一篇
Day 23 - output 函數介紹
下一篇
Day 25 - 使用outputFromObservable函數將Observable轉換為OutputRef
系列文
Signal API in Angular39
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言